1.【預測再驗證|生命週期與程序】 把舊 App 的三層接力(前景服務 → 綁定管理員 → ViewModel → LiveData)搬進新架構,最省事的骨架大概是這樣:core:location 只露一個介面,app 用前景服務實作,畫面用 LifecycleStartEffect 說「我需要定位了/不需要了」:
// core:location
interface LocationTracker {
val location: StateFlow<Location?>
fun acquire() // 一個畫面開始需要定位
fun release() // 那個畫面不再需要
}
// app:用前景服務實作
@Singleton
class ServiceLocationTracker @Inject constructor(
@ApplicationContext private val context: Context,
) : LocationTracker {
private val _location = MutableStateFlow<Location?>(null)
override val location = _location.asStateFlow()
private var clients = 0
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = (binder as LocationService.LocalBinder).service
CoroutineScope(Dispatchers.Main).launch {
service.location.collect { _location.value = it }
}
}
override fun onServiceDisconnected(name: ComponentName) = Unit
}
override fun acquire() {
if (clients++ == 0) {
val intent = Intent(context, LocationService::class.java)
context.startForegroundService(intent)
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun release() {
if (--clients == 0) context.unbindService(connection)
}
}
// feature:map
@Composable
fun TaskMapScreen(viewModel: TaskMapViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
LifecycleStartEffect(Unit) {
viewModel.onEvent(TaskMapUiEvent.Shown)
onStopOrDispose { viewModel.onEvent(TaskMapUiEvent.Hidden) }
}
// ViewModel 在 Shown 時 tracker.acquire()、Hidden 時 tracker.release()
}
服務本身照舊 App 的規則:最後一個客戶端解除綁定時先停掉定位更新,兩秒寬限期後沒人回來就自己停掉並收通知;onStartCommand 回 START_STICKY,但收到 null intent 時若沒有客戶端就 stopSelf()。
下面每個情境,先寫下你的預測與依據,再驗證(本專案的 demo flavor 加一個暫時的畫面就夠;通知欄、adb shell dumpsys activity services <package>、adb shell am kill <package>、Android 14 以上的實機或模擬器都用得到):
onStopOrDispose 和新畫面的 LifecycleStartEffect,哪個先跑?clients 有沒有一瞬間是 0?服務的兩秒寬限期會不會啟動、通知會不會閃一下?NavHost 有轉場動畫和沒有轉場動畫,答案一樣嗎?clients 是多少、服務還活著嗎、通知還在嗎?acquire() 在 Activity 的 onStart 時間點呼叫 startForegroundService(),Android 12 以上允許嗎?如果使用者這十分鐘在系統設定把定位權限關掉了,Android 14 上 startForeground(type = location) 會丟什麼?哪一層接得到、地圖畫面看到什麼?adb shell am kill <package> 殺掉程序(不要用「不保留活動」,那不會殺程序)。START_STICKY 讓系統用 null intent 重啟服務,骨架會做什麼?三十秒後通知欄有沒有東西?沒有客戶端的服務被重啟又自殺,宣告 START_STICKY 的意義是什麼?onTaskRemoved 在這個流程裡有沒有被叫到?onServiceConnected 裡那個 CoroutineScope(Dispatchers.Main).launch { … collect } 沒有人取消。綁定、解除綁定五次之後,活著的 collector 有幾個?解除綁定後 _location 是停在最後一個值,還是變回 null?使用者揹著手機走五百公尺再回到 App,地圖第一幀畫的是哪個位置?LeakCanary 預設不監看 Service 物件,所以這個情境它會不會叫?不會的話你要怎麼抓?@Singleton 的 tracker 活著,但 LifecycleStartEffect 會先 Hidden 再 Shown,clients 走 1 → 0 → 1。服務的寬限期計時器啟動了嗎?「因為 Activity 要重建而 release」和「因為使用者離開而 release」,tracker 分得出來嗎?分不出來的話,Activity.isChangingConfigurations 這個資訊該不該進 core:location?最後回答:這五個情境裡,哪幾個可以在 JVM 單元測試裡用假的服務連線對著 tracker 驗?哪幾個要 Robolectric 或 instrumented test?哪幾個只能實機驗、要寫進發版前的手動檢查清單?
我的回答:(待補)
2.【決策邊界|規格與驗收】 舊 App 的定位是這樣接的:一個約 310 行的前景服務握著 fused client(30 秒一次、最快 15 秒、高精度),對外露一個 StateFlow<Location?>;一個約 110 行的管理員負責綁定服務,在一個永遠不取消的 Main scope 裡把服務的 StateFlow 轉抄進自己的 StateFlow;一個 AndroidViewModel 再轉抄一次成 LiveData;四個畫面觀察它:地圖(我的位置)、智慧排序列表(顯示車輛座標)、拖曳排序、簽到。基底 Activity 在 onResume 時有權限就啟動並綁定、onStop 時解除綁定;服務在最後一個客戶端離開兩秒後自己停掉;一個 static volatile 的「服務在跑」旗標擋重複啟動。manifest 同時宣告了背景定位權限。
簽到畫面讀的是 liveData.value:按下簽到那一刻拿不到定位(null 或 0,0)就改送公司總部的座標,照樣送出。而且簽到畫面用的是自己那一份 ViewModel 實例,它背後的管理員從來沒有人叫它綁定服務。
新架構要先決定「定位屬於誰」。三個選項:
core:location 只是把 fused client 包成 callbackFlow 的 Flow<Location>,畫面在 STARTED 時收集、離開就停;沒有通知、沒有背景追蹤。LocationRequest 與同一個 Flow 介面,畫面分不出現在是哪一段。不看下面的大綱,依序回答:
foregroundServiceType="location" 的那一句話。寫不出來的話,剩下哪個選項?null」這種事,新架構要靠哪一層讓它在測試裡藏不住?core:location?沒有權限時 core:location 對外是 null、一個錯誤狀態,還是把「沒權限/關閉定位/有值」做成 sealed 型別讓畫面不得不處理?最後用兩句話收尾:你的結論,以及你願意承認的代價。
我的回答:(待補)